from collections import namedtuple

class ConfigSettings(object):
    r"""
    Used to store and retrieve configuration settings, utilizing default values wherever possible.

    >>> cfg = ConfigSettings({'a':'a', 'c':'c', 'd':'d'}, b='B', e='E', f='F')
    >>> cfg.set(c='N/A')
    >>> cfg.set('e', 'N/A')
    >>> cfg.set('e', '-new-', False)
    >>> cfg.set('d', value='-new-', override=False)
    >>> cfg.set(f='I have a context!', context=('my-context', 'meta-data'))
    >>> cfg
    ConfigSettings(
        a   = 'a',
        b   = 'B',
        c   = 'c',
        d   = 'd',
        e   = 'E',
        f   = 'F'
    ).update({
        'c' : 'N/A',
        'd' : '-new-',
        'e' : 'N/A'
        },
        context=None
    ).update({
        'f' : 'I have a context!'
        },
        context=('my-context', 'meta-data')
    )
    #   a   = 'a'
    #   b   = 'B'
    #   c   = 'N/A'
    #   d   = '-new-'
    #   e   = 'N/A'
    #   f   = 'I have a context!'
    """

    Value = namedtuple('Value', 'value context')

    def __init__(self, *args, **kwargs):
        r"""
        Takes any number of dictionaries and keyword arguments and uses them to fill out the default values for settings

        >>> ConfigSettings({'a':'a', 'b':'c'}, c='C', d='D')
        ConfigSettings(
            a   = 'a',
            b   = 'c',
            c   = 'C',
            d   = 'D'
        )
        #   a   = 'a'
        #   b   = 'c'
        #   c   = 'C'
        #   d   = 'D'
        """

        object.__init__(self)

        object.__setattr__(self, '_defaults', {})
        object.__setattr__(self, '_settings', {})

        self.set_defaults(*args, **kwargs)

    def __getattr__(self, name):
        r"""
        Syntax sugar: value = config.named_value
        Gets the value for the given setting, returning the default if no other value is available.
        raises AttributeError if the given name is not a valid setting.

        >>> cfg = ConfigSettings(a='value')
        >>> cfg.a
        'value'
        >>> cfg.b
        Traceback (most recent call last):
            ...
        AttributeError: b
        """
        try:
            return self[name]
        except KeyError, _:
            raise AttributeError, name

    def __setattr__(self, name, value):
        r"""
        syntax sugar: config.named_value = value
        Sets the value for the given setting, overriding all others.
        raises AttributeError if the given name is not a valid setting.

        >>> cfg = ConfigSettings(a='value')
        >>> cfg.a = 'new value'
        >>> cfg
        ConfigSettings(
            a   = 'value'
        ).update({
            'a' : 'new value'
            },
            context=None
        )
        #   a   = 'new value'
        >>> cfg.b = 'this should fail'
        Traceback (most recent call last):
            ...
        AttributeError: b
        """
        try:
            self[name] = value
        except KeyError, _:
            raise AttributeError, name
            #object.__setattr__(self, name, value)

    def __delattr__(self, name):
        r"""
        syntax sugar: del config.named_value
        Deletes the value for the given setting, allowing the default value to show through.
        raises AttributeError if the given name is not a valid setting.

        >>> cfg = ConfigSettings(a='value')
        >>> cfg.a = 'new value'
        >>> del cfg.a
        >>> cfg
        ConfigSettings(
            a   = 'value'
        )
        #   a   = 'value'
        >>> del cfg.b
        Traceback (most recent call last):
            ...
        AttributeError: b
        """
        try:
            del self[name]
        except KeyError, _:
            object.__delattr__(self, name)

    def __getitem__(self, name):
        r"""
        syntax sugar: value = config[name]
        Gets the value of a specific setting, returns the default if no other value is available
        raises KeyError if the given name is not a valid setting.

        >>> cfg = ConfigSettings(a='value a', b='value b')
        >>> cfg.b = 'new value'
        >>> cfg['a']
        'value a'
        >>> cfg['b']
        'new value'
        >>> cfg['c']
        Traceback (most recent call last):
            ...
        KeyError: 'c'
        """
        if name in self._settings:
            return self._settings[name].value
        if name in self._defaults:
            return self._defaults[name]
        raise KeyError, name

    def __setitem__(self, name, value):
        r"""
        syntax sugar: config[name] = value
        Sets the value of a specific setting, overriding all others.
        raises KeyError if the given name is not a valid setting.

        >>> cfg = ConfigSettings(a='value a', b='value b')
        >>> cfg['b'] = 'new value'
        >>> cfg['b']
        'new value'
        >>> cfg['c'] = 'this should fail'
        Traceback (most recent call last):
            ...
        KeyError: 'c'
        """
        if name in self._defaults:
            self._settings[name] = self.Value(value, None)
        else:
            raise KeyError, name

    def __delitem__(self, name):
        r"""
        syntax sugar: del config[name]
        Sets the value of a specific setting, overriding all others.
        raises KeyError if the given name is not a valid setting.

        >>> cfg = ConfigSettings(a='value a', b='value b')
        >>> cfg['b'] = 'new value'
        >>> cfg
        ConfigSettings(
            a   = 'value a',
            b   = 'value b'
        ).update({
            'b' : 'new value'
            },
            context=None
        )
        #   a   = 'value a'
        #   b   = 'new value'
        >>> del cfg['a']
        >>> del cfg['b']
        >>> cfg
        ConfigSettings(
            a   = 'value a',
            b   = 'value b'
        )
        #   a   = 'value a'
        #   b   = 'value b'
        >>> del cfg['c']
        Traceback (most recent call last):
            ...
        KeyError: 'c'
        """
        if name in self._defaults:
            if name in self._settings:
                del self._settings[name]
        else:
            raise KeyError, name

    def __contains__(self, name):
        r"""
        Syntax sugar: 'name' in config
        Returns true if the given setting has been overriden.
        rases KeyError if name is not a valid setting

        >>> cfg = ConfigSettings(a='value a', b='value b').update({'a':'-new-'})
        >>> 'a' in cfg
        True
        >>> 'b' in cfg
        False
        """
        return not self.is_default(name)

    def has_default(self, name):
        r"""
        Returns true if the given setting is a valid setting (has a default value)

        >>> cfg = ConfigSettings(a='value a')
        >>> cfg.has_default('a')
        True
        >>> cfg.has_default('b')
        False
        """
        return name in self._defaults

    def is_default(self, name):
        r"""
        Returns true if the given setting has not been overriden.
        rases KeyError if name is not a valid setting

        >>> cfg = ConfigSettings(a='value a', b='value b')
        >>> cfg.set(b='-new-')
        >>> cfg.is_default('a')
        True
        >>> cfg.is_default('b')
        False
        """
        if name not in self._defaults:
            raise KeyError, name
        return name not in self._settings

    def keys(self):
        r"""
        Returns a list of all setting names

        >>> cfg = ConfigSettings(a='value a', b='value b', c='value c')
        >>> keys = cfg.keys()
        >>> keys.sort()
        >>> keys
        ['a', 'b', 'c']
        """
        return self._defaults.keys()

    def iterkeys(self):
        r"""
        Iterates over each setting name

        >>> cfg = ConfigSettings(a='value a', b='value b', c='value c')
        >>> keys = []
        >>> for key in cfg.iterkeys():
        ...     keys.append(key)
        >>> keys.sort()
        >>> keys
        ['a', 'b', 'c']
        """
        return self._defaults.iterkeys()

    def values(self):
        r"""
        Returns a list of all setting values

        >>> cfg = ConfigSettings(a='value a', b='value b', c='value c')
        >>> values = cfg.values()
        >>> values.sort()
        >>> values
        ['value a', 'value b', 'value c']
        """
        return list(self.itervalues())

    def itervalues(self):
        r"""
        Iterates over each setting value

        >>> cfg = ConfigSettings(a='value a', b='value b', c='value c')
        >>> values = []
        >>> for value in cfg.itervalues():
        ...     values.append(value)
        >>> values.sort()
        >>> values
        ['value a', 'value b', 'value c']
        """
        for k in self.iterkeys():
            yield self[k]

    def items(self):
        r"""
        Returns a list of all items as (name, value) pairs

        >>> cfg = ConfigSettings(a='value a', b='value b', c='value c')
        >>> items = cfg.items()
        >>> items.sort()
        >>> items
        [('a', 'value a'), ('b', 'value b'), ('c', 'value c')]
        """
        return list(self.iteritems())

    def iteritems(self):
        r"""
        Iterates over all items, yielding (name, value) pairs

        >>> cfg = ConfigSettings(a='value a', b='value b', c='value c')
        >>> items = []
        >>> for item in cfg.iteritems():
        ...     items.append(item)
        >>> items.sort()
        >>> items
        [('a', 'value a'), ('b', 'value b'), ('c', 'value c')]

        """
        for k in self.iterkeys():
            yield k, self[k]

    def get(self, *args, **kwargs):
        r"""
        Returns the value of the requested setting, or the provided default if it has not been overriden, or finally the default value for that setting.

        get(name)
        get(name, default)
        get(name, default=value)
        get(name=default_value) # where name is the name of the setting and default_value is the default_value for that setting

        >>> cfg = ConfigSettings(a='default a', b='default b')
        >>> cfg.set(b='override b')
        >>> cfg.get('a')
        'default a'
        >>> cfg.get('a', 'provided default')
        'provided default'
        >>> cfg.get('a', default='provided default')
        'provided default'
        >>> cfg.get(a='provided default')
        'provided default'
        >>> cfg.get('b')
        'override b'
        >>> cfg.get('b', 'provided default')
        'override b'
        >>> cfg.get('b', default='provided default')
        'override b'
        >>> cfg.get(b='provided default')
        'override b'
        """
        name        = None
        default     = None
        has_default = False

        # (self, name)
        if len(args) == 1 and len(kwargs) == 0:
            name = args[0]
        # (self, name, default)
        elif len(args) == 2 and len(kwargs) == 0:
            name, default = args
            has_default = True
        # (self, name, default=value)
        elif len(args) == 1 and len(kwargs) == 1 and ('default' in kwargs):
            name = args[0]
            default = kwargs['default']
            has_default = True
        # (self, name=value)
        elif len(args) == 0 and len(kwargs) == 1:
            name  = kwargs.keys()[0]
            default = kwargs[name]
            has_default = True
        # Error
        else:
            raise SyntaxError, 'invalid arguments'

        if name not in self._defaults:
            raise KeyError, name

        if name in self._settings:
            return self._settings[name].value

        if has_default:
            return default

        return self._defaults[name]


    # (self, name, value)
    # (self, name, value, override)
    # (self, name, value, override, context)
    # (self, name, value, override, context=XXX)
    # (self, name, value, context, override=XXX)
    # (self, name, override, context, value=XXX)
    # (self, name, value, override=XXX, context=XXX)
    # (self, name, override, value=XXX, context=XXX)
    # (self, name, context, value=XXX, override=XXX)
    # (self, name, value=XXX, override=XXX, context=XXX)
    # (self, name=value)
    # (self, override, name=value)
    # (self, override, context, name=value)
    # (self, name=value, override=XXX)
    # (self, name=value, context=XXX)
    # (self, name=value, override=XXX, context=XXX)

    def set(self, *args, **kwargs):
        r"""
        Sets the value for the given setting. If override is false, then the value will only be changed if it is not already overriden.

        set(name, value)
        set(name, value, override)
        set(name, value, override, context)
        set(name, value, override, context=XXX)
        set(name, value, context, override=XXX)
        set(name, override, context, value=XXX)
        set(name, value, override=XXX, context=XXX)
        set(name, override, value=XXX, context=XXX)
        set(name, context, value=XXX, override=XXX)
        set(name, value=XXX, override=XXX, context=XXX)
        set(name=value)
        set(override, name=value)
        set(override, context, name=value)
        set(name=value, override=XXX)
        set(name=value, context=XXX)
        set(name=value, override=XXX, context=XXX)

        >>> cfg = ConfigSettings(a='default a', b='default b')
        >>> cfg.set('a', '#1')
        >>> cfg.a
        '#1'
        >>> cfg.set('a', '#2', False)
        >>> cfg.a
        '#1'
        >>> cfg.set('a', '#3', True, 'context')
        >>> cfg.a
        '#3'
        >>> cfg.context('a')
        'context'
        >>> cfg.set(a='#4', context='context')
        >>> cfg.a
        '#4'
        >>> cfg.context('a')
        'context'
        """

        args = list(args)
        name  = None
        value = None
        override = True
        context = None

        if len(args) == 4:
            context = args.pop()
        elif 'context' in kwargs:
            context = kwargs.pop('context')

        if len(args) == 3:
            override = args.pop()
        elif 'override' in kwargs:
            override = kwargs.pop('override')

        if len(args) == 2:
            value = args.pop()
        elif 'value' in kwargs:
            value = kwargs.pop('value')

        if len(args) == 1:
            name = args.pop()
        else:
            name, value = kwargs.popitem()

        if len(args) != 0 or len(kwargs) != 0:
            raise SyntaxError, 'invalid arguments'

        if name not in self._defaults:
            raise KeyError, name

        if override or (name not in self._settings):
            self._settings[name] = self.Value(value, context)

    def set_default(self, name, value):
        r"""
        Creates a new setting, with the specified default value
        raises KeyError if the setting already exists

        >>> cfg = ConfigSettings(a='value a')
        >>> cfg.set_default('b', 'value b')
        >>> cfg
        ConfigSettings(
            a   = 'value a',
            b   = 'value b'
        )
        #   a   = 'value a'
        #   b   = 'value b'
        >>> cfg.set_default('a', '-new default-')
        Traceback (most recent call last):
            ...
        KeyError: 'a already has a default'
        """
        if name in self._defaults:
            raise KeyError, '%s already has a default' % name
        self._defaults[name] = value

    def set_defaults(self, *args, **kwargs):
        r"""
        Adds a dictionary of new settings.
        raises KeyError if any of the new settings already exist.

        >>> cfg = ConfigSettings(a='value a')
        >>> cfg.set_defaults({'b':'value b'}, c='value c')
        >>> cfg
        ConfigSettings(
            a   = 'value a',
            b   = 'value b',
            c   = 'value c'
        )
        #   a   = 'value a'
        #   b   = 'value b'
        #   c   = 'value c'
        """
        for arg in args:
            for k, v in arg.iteritems():
                self.set_default(k, v)
        for k, v in kwargs.iteritems():
            self.set_default(k, v)

    def get_default(self, name):
        r"""
        Returns the default value for the given setting
        raises KeyError if the setting does not exist

        >>> cfg = ConfigSettings(a='value a')
        >>> cfg.a = 'new value'
        >>> cfg.a
        'new value'
        >>> cfg.get_default('a')
        'value a'
        """
        return self._defaults[name]

    def update(self, values, override=True, context=None):
        r"""
        Updates all the settings with new values (depending on the override).

        >>> cfg = ConfigSettings(a='value a')
        >>> cfg.update({'a':'-new-'})
        ConfigSettings(
            a   = 'value a'
        ).update({
            'a' : '-new-'
            },
            context=None
        )
        #   a   = '-new-'
        """
        for k, v in values.iteritems():
            self.set(k,v,override,context)
        return self

    def context(self, name):
        r"""
        Returns the context for the specified setting, or 'default' if it was not overriden

        >>> cfg = ConfigSettings(a='value a', b='value b')
        >>> cfg.set(b='-new-', context='new context')
        >>> cfg.context('a')
        'default'
        >>> cfg.context('b')
        'new context'
        """
        if name in self._settings:
            return self._settings[name].context
        if name in self._defaults:
            return 'default'
        raise KeyError, name

    def __str__(self):
        keys = list(self.keys())
        keys.sort()
        padd = max(map(len, keys))

        return 'ConfigSettings(\n' +\
            ''.join('    %-*s = %r,\n' % (padd, k, self[k]) for k in keys) +\
            ')'

    def __repr__(self):
        def_keys = list(self._defaults.keys())
        def_keys.sort()

        set_keys = list(self._settings.keys())
        set_keys.sort()

        contexts = set()
        for v in self._settings.itervalues():
            contexts.add(v.context)
        contexts = list(contexts)
        contexts.sort()

        fin_keys = list(self.keys())
        fin_keys.sort()
        padd = max(map(len, fin_keys)) + 2

        return 'ConfigSettings(' +\
            ','.join('\n    %-*s = %r' % (padd, k, self._defaults[k]) for k in def_keys) +\
            '\n)' +\
            ''.join(
                '.update({%s\n    },\n    context=%r\n)' % (
                    ','.join('\n    %-*r : %r' % (
                        padd,
                        k,
                        self._settings[k].value
                    ) for k in set_keys if self._settings[k].context == context),
                    context
                ) for context in contexts
            ) +\
            ''.join(
                '\n#   %-*s = %s' % (
                    padd,
                    k,
                    ('%r' % self[k]).replace('\n', '\n#       ')
                ) for k in fin_keys
            )

if __name__ == '__main__':
    import doctest
    doctest.testmod()